マンガデータの量を見る#

マンガデータを例に、量を見るためのデータ可視化手法を学びましょう:

質的変数の量を見る最も一般的な方法は 棒グラフ を作図することです。 複数の質的変数を扱う場合は、集合棒グラフ積上げ棒グラフも効果的です。 前者は特に絶対値を比較したい場合、後者は特に割合を比較したい場合に便利です。 全体像を俯瞰したい場合は、ヒートマップを選択肢に入れましょう。 ただし、ヒートマップは質的変数の組合せの量を色で表現するため、数値を付記しなければ絶対値の比較が難しいことにご注意ください。

なお、データ可視化手法の選定に関しては、Claus O. Wilke, Fundamentals of Data Visualization5.1 Amountsを参考にしました。

有意義なデータ可視化を行うために重要なポイントの一つは、 仮説を立てる ことです[貴裕, 2023]。 ドメイン知識を利用して仮説を立て、可視化によって誤りがないか確認[1]し次の分析に繋げることで、効果的に作業を進めることができます。 本ハンズオンではこの考えに則り、各可視化手法を取り上げる前に可能な限り[2]仮説を設定します[3]

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 様々なパターンのループを効率的に実行可能
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

Pythonにおける型ヒント

「型ヒント」とは、Pythonのコードにおいて、変数や関数の引数、返り値の型を示すためのヒントを提供するものです。 これにより、コードの可読性が向上したり、型チェッカーなどのツールがエラーを検出しやすくなります。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータが保存されているディレクトリのパス
DIR_IN = Path("../../data/cm/input")

# 分析結果の出力先ディレクトリのパス
DIR_OUT = DIR_IN.parent / "output" / Path.cwd().parts[-1] / "amounts"
Hide code cell content
# 読み込み対象ファイル

# Comic Episode関連のファイル名
FN_CE = "cm_ce.csv"
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"

関数#

本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = 10, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトは10
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def resample_df_by_col_and_years(df: pd.DataFrame, col: str) -> pd.DataFrame:
    """
    指定されたカラムと年数に基づき、データフレームを再サンプル
    colとyearsの全ての組み合わせが存在するように0埋めを行う
    この処理は、作図時にX軸方向の順序が変わることを防ぐために必要

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    col : str
        サンプリング対象のカラム名

    Returns
    -------
    pd.DataFrame
        再サンプルされたデータフレーム
    """

    # 入力データフレームを新しい変数にコピー
    df_new = df.copy()

    # データフレームからユニークな年数一覧を取得
    unique_years = df["years"].unique()

    # データフレームからユニークなcolの値一覧を取得
    unique_vals = df[col].unique()

    # 一意なカラムの値と年数の全ての組み合わせに対して処理
    for val, years in itertools.product(unique_vals, unique_years):
        # 対象のカラムの値と年数が一致するデータを抽出
        df_tmp = df_new[(df_new[col] == val) & (df_new["years"] == years)]

        # 該当するデータが存在しない場合
        if df_tmp.shape[0] == 0:
            # 0埋めのデータを作成
            default_data = {c: 0 for c in df_tmp.columns}
            # col列についてはvalで埋める
            default_data[col] = val
            # years列についてはyearで埋める
            default_data["years"] = years
            # 新たなレコードとして追加
            df_add = pd.DataFrame(default_data, index=[0])

            # 0埋めのデータをデータフレームに追加
            df_new = pd.concat([df_new, df_add], ignore_index=True)

    return df_new
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

まず、可視化対象となるデータを読み込みましょう。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_IN / FN_CE)
Hide code cell content
# date列をdatetime型に変換
df_ce["date"] = pd.to_datetime(df_ce["date"])

棒グラフ#

本書で扱うマンガ作品の中で最も話数が多い作品はどれでしょうか? 筆者の知る限り、最も長寿な作品はこちら葛飾区亀有公園前派出所です。 本項では、棒グラフを用いて、「本書で扱うマンガ作品の中で最も各話数が多いものはこちら葛飾区亀有公園前派出所である」という仮説を検証します。

棒グラフBar Chart ) とは、主に質的変数と対応する数量を 棒の長さ で表す可視化手法です。 棒を縦方向に並べることもありますし、横方向に並べることもあります。 質的変数の量を見る最も一般的な方法の一つです。 詳細は5章を参照ください。

Hide code cell content
# マンガ作品ごとの掲載話数を集計するためのDataFrameを作成
# 'groupby' と 'nunique' を使用して、各マンガ作品ごとにユニークな 'ceid'(掲載話数)を数える
df_bar = df_ce.groupby(["ccname"])["ceid"].nunique().reset_index(name="n_ce")

# 掲載話数が多い順にデータを並び替えて、上位N_CC件のデータを選択
# 'sort_values' でソートし、'head(20)' で上位20件を取得
df_bar = df_bar.sort_values(by="n_ce", ascending=False, ignore_index=True).head(20)

# 可視化のために'rename'メソッドを用いて列名をわかりやすい名前に変更
df_bar = df_bar.rename(columns={"ccname": "マンガ作品名", "n_ce": "合計話数"})
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_bar.head()
マンガ作品名 合計話数
0 こちら葛飾区亀有公園前派出所 1968
1 はじめの一歩 1186
2 名探偵コナン 1009
3 ONE PIECE 893
4 MAJOR 748
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bar, DIR_OUT, "bar")
DataFrame is saved as '../../data/cm/output/02/amounts/bar.csv'.
Hide code cell source
# 'px.bar' を使用して棒グラフを作成
# x軸には '合計話数'、y軸には 'マンガ作品名' を設定
# 'orientation' を 'h' に設定することで、棒グラフを水平(横向き)に表示
# 'height' でグラフの高さを指定
fig = px.bar(df_bar, x="合計話数", y="マンガ作品名", orientation="h", height=500)

# 作成した棒グラフを表示
show_fig(fig)

上図は、マンガ作品ごとの合計話数を表現した棒グラフです。 こちら葛飾区亀有公園前派出所に代表されるように作品名が長い作品が存在するため、横方向に棒を伸ばした 横棒グラフ を採用しました。

棒グラフを見る限り、こちら葛飾区亀有公園前派出所ほど各話数が多いマンガ作品は存在しないように見えます。 「本書で扱うマンガ作品の中で最も各話数が多いものはこちら葛飾区亀有公園前派出所である」という仮説と整合性のある結果が得られました。 こちら葛飾区亀有公園前派出所はアンケート至上主義の週刊少年ジャンプにて1976年6月から2016年まで 一度の休載もなく続いた 驚異的な作品[修治, 2020]です。

ccidではなくccnameで集計する理由

通常このような集計を行う際、ccidのようなIDを用いることも考えられます。 しかし、基礎分析の結果、ccidccnameは1対1で対応しておらず(厳密にはn対1で対応しており)、ccidで集計するとMAJORのような大ヒット作品を分割してしまうことがわかっています。

非常に悩みましたが、

  • 今回の分析用途であれば、集計ミスがあってもドメイン知識で検出できる

  • MAJORのような大ヒット作品を適切に扱えないのは許容できない

という二点の理由からccnameで集計することにしました。 このように、状況に応じて、集計方法を柔軟に変更することがあります。

以降の分析でも、特に断らずにccnameで集計することがありますので、ご了承ください。

集合棒グラフ#

繰り返しになりますが、こちら葛飾区亀有公園前派出所の驚異的な点の一つは、40年以上の連載期間中一度も休載しなかった点です。 そこで本項では、集合棒グラフを用いて「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど長期間コンスタントに連載したものはない」という仮説を検証します。

集合棒グラフGrouped Bar Chart ) とは、新たな質的変数に応じて棒グラフの内訳を分割し、 並列に並べた 可視化手法です。 詳細は5章を参照ください。

ここでは、年代 ごとに分割することで仮説を確かめましょう。

Hide code cell content
# 10年区切りの年代情報を追加
df_ce = add_years_to_df(df_ce)
Hide code cell content
# 「ccname」と「years」列で集計し、ユニークな「ceid」の数を「n_ce」列に格納
df_gbar = df_ce.groupby(["ccname", "years"])["ceid"].nunique().reset_index(name="n_ce")

# df_barに含まれるから「マンガ作品名」の上位10作品をリストに格納
ccnames = list(df_bar["マンガ作品名"])[:10]
# df_gbarをフィルタリングして、ccnamesに含まれるccnameのみを選択
df_gbar = df_gbar[df_gbar["ccname"].isin(ccnames)].reset_index(drop=True)
# 「ccname」列と「years」列を使ってdf_gbarをリサンプリング
df_gbar = resample_df_by_col_and_years(df_gbar, "ccname")

# 「ccname」列をカテゴリカルデータとして扱い、ccnamesの順序で並べ替え
df_gbar["ccname"] = pd.Categorical(df_gbar["ccname"], categories=ccnames, ordered=True)
# 「ccname」と「years」でソートし、インデックスをリセット
df_gbar = df_gbar.sort_values(["ccname", "years"], ignore_index=True)

# 可視化用に列名をわかりやすい日本語の名前に変更
df_gbar = df_gbar.rename(
    columns={"ccname": "マンガ作品名", "years": "年代", "n_ce": "合計話数"}
)
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_gbar.head()
マンガ作品名 年代 合計話数
0 こちら葛飾区亀有公園前派出所 1970 160
1 こちら葛飾区亀有公園前派出所 1980 501
2 こちら葛飾区亀有公園前派出所 1990 482
3 こちら葛飾区亀有公園前派出所 2000 492
4 こちら葛飾区亀有公園前派出所 2010 333
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_gbar, DIR_OUT, "gbar")
DataFrame is saved as '../../data/cm/output/02/amounts/gbar.csv'.
Hide code cell source
# df_gbarデータを使用して棒グラフを作成
# Y軸にマンガ作品名をとり、X軸に合計話数をとり、色は年代別に表示
# orientationで水平方向の横棒グラフを指定し、barmode=groupで集合棒グラフ化
# グラフの高さは600に調整し、カラーパレットはPortlandを指定
fig = px.bar(
    df_gbar,
    x="合計話数",
    y="マンガ作品名",
    color="年代",
    orientation="h",
    barmode="group",
    height=600,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に固定
# yanchorとxanchorは凡例の基準点(top:上部、right:右端)を指定
# yとxはその基準点の位置を指定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# 集合棒グラフを表示
show_fig(fig)

上図は、年代別のマンガ作品の合計話数を表現した集合棒グラフです。作品名が長いものが多いため、横方向に棒を伸ばした横棒グラフとしました。 こちら葛飾区亀有公園前派出所1980年代から2000年代まで、全て500話近く掲載していることがわかります。 1970年代および2010年代は、連載開始年と終了年が含まれるため例外的に合計話数が小さくなっていることに注意してください。

他の偉大なマンガ作品と比較しても、複数の年代にわたり500話近くを掲載した作品はありません。 「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど長期間コンスタントに連載したものはない」という仮説と整合性のある結果が得られました。

上図を更に観察すると、集合棒グラフには次のような長所があることがわかります:

  • 各作品・各年代の絶対値を比較しやすい

    • 例:1970年代はダメおやじ1980年代はこちら葛飾区亀有公園前派出所が代表的

  • 各作品がどの年代に掲載されたか定性的にわかりやすい

    • 例:ダメおやじ等は1970-1980年代、MAJOR1990-2010年代に掲載された

一方で,次のような短所があることもわかります:

  • 年代の数に比例して凡例の数が増えてしまうため、全体的に棒が細くなり視認性が悪い

  • 年代をまたがった合計掲載週数の比較がしづらい

ちなみに、本書では質的変数の中でも、順序尺度と名義尺度でカラースケールを使い分けます。 具体的には、年代等の順序尺度の場合はpx.colors.diverging.Portlandを、マンガ作者名等の名義尺度の場合は岡部・伊藤カラースケール[Okabe and Ito, 2008]を採用します。

group対象に欠損があるとX軸の順序が自動調整されてしまう

おそらくpx.bar()の仕様ですが,barmode="group"あるいはbarmode="stack"を選択した際にcolorで指定した列に欠損があると、X軸の順序が変わってしまうことを確認しました。 これを回避するため、resample_df_by_cname_and_years(df)で欠測を補完しています。 以降も同様です。

積上げ棒グラフ#

集合棒グラフと対になる可視化手法として、積上げ棒グラフがあります。 本項では、積上げ棒グラフを用いて「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど長期間コンスタントに連載したものはない」という仮説を再度検証してみましょう。

積上げ棒グラフStacked Bar Chart ) とは、 新たな質的変数に応じて棒グラフの内訳を分割し、 直列に並べた 可視化手法です。 詳細は5章を参照ください。

ここでも、集合棒グラフと同様、 年代 で棒グラフの各要素を分割します。

Hide code cell content
# 比較のため、集合棒グラフと同じDataFrameを利用
df_sbar = df_gbar.copy()
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_sbar.head()
マンガ作品名 年代 合計話数
0 こちら葛飾区亀有公園前派出所 1970 160
1 こちら葛飾区亀有公園前派出所 1980 501
2 こちら葛飾区亀有公園前派出所 1990 482
3 こちら葛飾区亀有公園前派出所 2000 492
4 こちら葛飾区亀有公園前派出所 2010 333
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_sbar, DIR_OUT, "sbar")
DataFrame is saved as '../../data/cm/output/02/amounts/sbar.csv'.
Hide code cell source
# df_gbarデータを使用して棒グラフを作成
# Y軸にマンガ作品名をとり、X軸に合計話数をとり、色は年代別に表示
# orientationで水平方向の横棒グラフを指定し、barmode=stackで積上げ棒グラフ化
# グラフの高さは400に調整し、カラーパレットはPortlandを指定
fig = px.bar(
    df_gbar,
    x="合計話数",
    y="マンガ作品名",
    color="年代",
    orientation="h",
    barmode="stack",
    height=400,
    color_discrete_sequence=px.colors.diverging.Portland,
)

# 凡例の位置を図の右上に固定
# yanchorとxanchorは凡例の基準点(top:上部、right:右端)を指定
# yとxはその基準点の位置を指定
fig.update_layout(legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99))

# 積上げ棒グラフを表示
show_fig(fig)

上図は、年代別のマンガ作品の合計話数を表現した積上げ棒グラフです。作品名が長いものが多いため、横方向に棒を伸ばした横棒グラフとしました。 こちら葛飾区亀有公園前派出所の合計話数の内訳が、(連載開始・終了を含む両端の二色を除き)均等に分割されていることがわかります。 他のマンガ作品は多かれ少なかれ特定の年代に偏っていますので、こちら葛飾区亀有公園前派出所の異常さが際立ちます。

集合棒グラフとは別の観点からですが、「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど長期間コンスタントに連載したものはない」という仮説と整合性のある結果が得られました。

ここまでの議論から、積上げ棒グラフの長所として以下があることがわかります:

  • 各作品の年代ごとの 内訳 [4]を比較しやすい

    • 例:銀魂の合計話数は、2000年代と2010年代で4:6程度の比率になっている

  • 各作品の合計掲載週を比較しやすい

    • 例:こちら葛飾区亀有公園前派出所の合計話数は、名探偵コナン[5]のそれの倍程度ある

  • 棒を補足する必要がないため、集合棒グラフと比較して小さい領域に効率的に描画できる

    • 例:集合棒グラフではheight=600で可視化した情報を、積上げ棒グラフではheight=400で十分可視化可能

一方で、積上げ棒グラフには次のような短所があることがわかります:

  • 各作品・各年代の絶対値を比較しづらい

つまり、積上げ棒グラフの特徴は集合棒グラフと表裏一体です。

ヒートマップ#

集合棒グラフ積上げ棒グラフでは、 10年 を最小単位として各話数を集計したため、各マンガ作品の細かい時間粒度の各話数の推移を確認することはできませんでした。 特に集合棒グラフでは、二つの年代に跨る期間に集中的に連載したマンガ作品に関して、実際よりも各話数が少ない印象を与えてしまう可能性があります。

そこで、本項ではヒートマップを用いて、より細かい時間粒度で各話数を可視化します。 検証する仮説は「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど 年間話数 を長期間高水準に維持したものはない」です。

ヒートマップHeatmap ) とは、主に複数の質的変数の組合せに対して、数量を で表す可視化手法です。 (具体的な数値をアノテーションしない限り)定量的な比較は難しいですが、全体像を俯瞰したい場合は非常に便利です。 また、非常に多くの組合せに対して数量を表現できる、という強みもあります。 詳細は5章を参照ください。

本項ではこの特長を利用して、 1年単位 の各話数を可視化します。

Hide code cell content
# 1年区切りの年代情報を追加
df_ce = add_years_to_df(df_ce, unit_years=1)

# df_ceをdate順でソートし、全ccnameを事前に抽出しておく、時間順に並んだヒートマップを作成するための工夫
ccnames = (
    df_ce.sort_values(["date", "ccid"], ignore_index=True)["ccname"].unique().tolist()
)
Hide code cell content
# 「ccname」と「years」と「mcname」列で集計し、ユニークな「ceid」の数を「n_ce」列に格納
df_hm = (
    df_ce.groupby(["mcname", "ccname", "years"])["ceid"]
    .nunique()
    .reset_index(name="n_ce")
)

# ccnamesのうち、df_barの「マンガ作品名」の上位10作品を抽出
# 直接list(df_bar["マンガ作品名"])[:20]を用いないのはccnamesの順序を維持するため
ccnames = [ccname for ccname in ccnames if ccname in list(df_bar["マンガ作品名"])[:20]]
# df_hmをフィルタリングして、ccnamesに含まれるccnameのみを選択
df_hm = df_hm[df_hm["ccname"].isin(ccnames)].reset_index(drop=True)
# 「ccname」列と「years」列を使ってdf_gbarをリサンプリング
df_hm = resample_df_by_col_and_years(df_hm, "ccname")

# 「ccname」列をカテゴリカルデータとして扱い、ccnamesの順序で並べ替え
df_hm["ccname"] = pd.Categorical(df_hm["ccname"], categories=ccnames, ordered=True)
# 「ccname」と「years」でソートし、インデックスをリセット
df_hm = df_hm.sort_values(["ccname", "years"], ignore_index=True)

# 可視化用に列名をわかりやすい日本語の名前に変更
df_hm = df_hm.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "ccname": "マンガ作品名",
        "years": "掲載年",
        "n_ce": "合計話数",
    }
)
Hide code cell content
# 可視化対象のDataFrameの内容を確認
df_hm.head()
マンガ雑誌名 マンガ作品名 掲載年 合計話数
0 週刊少年サンデー ダメおやじ 1970 9
1 週刊少年サンデー ダメおやじ 1971 51
2 週刊少年サンデー ダメおやじ 1972 51
3 週刊少年サンデー ダメおやじ 1973 50
4 週刊少年サンデー ダメおやじ 1974 50
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_hm, DIR_OUT, "hm")
DataFrame is saved as '../../data/cm/output/02/amounts/hm.csv'.
Hide code cell source
# df_hmデータを使ってヒートマップを作成
# x軸に掲載年、y軸にマンガ作品名、z軸に合計話数を設定
fig = px.density_heatmap(df_hm, x="掲載年", y="マンガ作品名", z="合計話数", height=600)

# ヒートマップを表示
show_fig(fig)

上図は、マンガ作品ごとの年間合計話数を表現したヒートマップです。色が明るいほど合計話数が多いことを表します。 1年間は約52週 ですので、週刊誌に毎週掲載されたとしても最大で52話となります。 さらに、週刊誌は大型連休のタイミングで合併号を出すことが多いので、現実的には最大でも50話程度となるでしょう。

5章で詳しく触れますが、ヒートマップでは横軸の変化に応じて縦軸をソートすると非常に見やすくなります。 上図では、連載開始が早い順に並び替えることで階段状になり、マンガ作品間の比較を容易にする狙いがあります。

下から3行目のこちら葛飾区亀有公園前派出所を確認しましょう。1977年の連載開始以来、色合いがほとんど変わらないまま2016年の連載終了を迎えていることがわかります。 他のマンガ作品はどれも素晴らしいものですが、ここまで長期間年間話数を維持したものはありません。 つまり、「本書で扱うマンガ作品の中で、こちら葛飾区亀有公園前派出所ほど 年間話数 を長期間高水準に維持したものはない」という仮説と整合性のある結果が得られました。

また、副産物的な発見ではありますが、2009年にこちら葛飾区亀有公園前派出所NARUTO-ナルト-BLEACH、そして銀魂で不自然に年間話数が増えていることがわかります。 これらのマンガ作品に共通するのは、全て週刊少年ジャンプの連載作品であるという点です。 そこで、2009年の週刊少年ジャンプのマンガ各話数を、マンガ雑誌巻号別に集計してみましょう。

Hide code cell content
# 週刊少年ジャンプの2009年のデータを選択し、雑誌巻号ごとにCEIDのユニーク数をカウント
# グループ化して、新しい列n_ceとして格納
df_mi = (
    df_ce[(df_ce["mcname"] == "週刊少年ジャンプ") & (df_ce["years"] == "2009")]
    .groupby(["miid", "miname"])["ceid"]
    .nunique()
    .reset_index(name="n_ce")
)

# n_ceで降順ソートして上位5件を表示
df_mi.sort_values("n_ce", ascending=False).head()
miid miname n_ce
47 M542881 週刊少年ジャンプ 2009年 表示号数6 44
0 M542834 週刊少年ジャンプ 2010年 表示号数03 43
18 M542852 週刊少年ジャンプ 2009年 表示号数37 37
43 M542877 週刊少年ジャンプ 2009年 表示号数11 26
17 M542851 週刊少年ジャンプ 2009年 表示号数39 24

週刊少年ジャンプ 2009年 表示号数6週刊少年ジャンプ 2010年 表示号数03および週刊少年ジャンプ 2010年 表示号数03において、通常の倍以上のマンガ作品が掲載されていたことがわかりました。 最も各話数の多い週刊少年ジャンプ 2009年 表示号数6を見てみましょう。

Hide code cell content
# miidが'M542881'であるレコードを選択し、必要な列のみを抽出
# ccname, cename, page_start, pages, dateを選択
# page_startでソートして、ページの開始順に並べ替え
df_ce[df_ce["miid"] == "M542881"][
    ["ccname", "cename", "page_start", "pages", "date"]
].sort_values("page_start")
ccname cename page_start pages date
39979 NARUTO-ナルト- 430:ナルト帰還!! 9.0 33.0 2009-01-28
39980 ONE PIECE 第527話 紅蓮地獄 43.0 19.0 2009-01-28
39981 トリコ グルメ 32 ロックドラム!! 63.0 19.0 2009-01-28
39982 BLEACH 340. The Antagonizer 83.0 19.0 2009-01-28
39983 家庭教師ヒットマンREBORN! 標的 224 XANXUS VS. Rasiel 103.0 17.0 2009-01-28
39984 ONE PIECE 消されたライセンス 130.0 1.0 2009-01-28
39985 こちら葛飾区亀有公園前派出所 コタツとみかん 130.0 1.0 2009-01-28
39986 ぬらりひょんの孫 1ゆら 2カナ 3つらら 131.0 1.0 2009-01-28
39988 BLEACH 白哉玉 132.0 1.0 2009-01-28
39987 魔人探偵脳噛ネウロ 弥子の正月 132.0 1.0 2009-01-28
39989 アスクレピオス はねつきっス!! 133.0 1.0 2009-01-28
39990 To LOVEる -とらぶる- ダーク・オ・セチー 134.0 1.0 2009-01-28
39991 アイシールド21 一富士二鷹三茄子は最高に縁起のいい初夢です 134.0 1.0 2009-01-28
39992 マイスター 蹴り初め 135.0 1.0 2009-01-28
39993 銀魂 汁粉と善哉 136.0 1.0 2009-01-28
39994 SKET DANCE 今年の目標 136.0 1.0 2009-01-28
39995 黒子のバスケ 本の虫 137.0 1.0 2009-01-28
39997 PSYREN-サイレン- おねがいごと 138.0 1.0 2009-01-28
39996 家庭教師ヒットマンREBORN! ランボはどこ? 138.0 1.0 2009-01-28
39998 ぼっけさん ヒノとサユの初詣 139.0 1.0 2009-01-28
40000 トリコ トリコの正月 140.0 1.0 2009-01-28
39999 バクマン。 俺達に正月休みはない! 140.0 1.0 2009-01-28
40001 いぬまるだしっ お正月の思い出作文 141.0 1.0 2009-01-28
40002 ピューと吹く!ジャガー ぼやき初め 142.0 1.0 2009-01-28
40003 NARUTO-ナルト- 10年目の真実 142.0 1.0 2009-01-28
40004 SKET DANCE 第72話 ファッショナブル侍 145.0 17.0 2009-01-28
40005 ダブルマメダイチ NaN 163.0 49.0 2009-01-28
40006 バクマン。 20ページ 未来と階段 213.0 21.0 2009-01-28
40007 銀魂 第243訓 何回見てもラピュタはいい 237.0 19.0 2009-01-28
40008 アイシールド21 312th down 新世代へ 257.0 19.0 2009-01-28
40009 いぬまるだしっ 第20回 1 「たまこ先生の年賀状」 299.0 3.0 2009-01-28
40010 いぬまるだしっ 第20回 2 「アレに似てるおじさん」 302.0 4.0 2009-01-28
40011 いぬまるだしっ 第20回 3 「善と悪」 306.0 2.0 2009-01-28
40012 黒子のバスケ 第4Q まともじゃないかもしんないスね 311.0 19.0 2009-01-28
40013 ぼっけさん 第3怪 存在の証明 331.0 27.0 2009-01-28
40014 マイスター Play.5 スタープレーヤー 359.0 19.0 2009-01-28
40015 こちら葛飾区亀有公園前派出所 初夢の正月クルーズの巻 379.0 19.0 2009-01-28
40016 PSYREN-サイレン- CALL.53 “痛み” 399.0 19.0 2009-01-28
40017 ぬらりひょんの孫 第41幕 百鬼夜行対八十八鬼夜行 419.0 19.0 2009-01-28
40018 魔人探偵脳噛ネウロ 第188話 距【きょり】 439.0 19.0 2009-01-28
40019 To LOVEる -とらぶる- トラブル 131 クイーンの反抗 459.0 19.0 2009-01-28
40020 アスクレピオス 第15話 結紮!! 481.0 19.0 2009-01-28
40021 ピューと吹く!ジャガー 映画公開記念特別編 ・~いま、吹きにゆきます~~の撮影にいま、ゆきます~ 511.0 1.0 2009-01-28
40022 ピューと吹く!ジャガー 週刊!?ハミ通SP 特集:文字で見る映画の裏側 512.0 2.0 2009-01-28

130ページから142ページにかけて、1ページあたり2作品ずつ掲載されていたことがわかりました。 発売時期や各話タイトルから察するに、お正月企画の4コマ漫画ではないでしょうか。